package com.tgd.RingtoneContactManager;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import android.app.Activity;
import android.content.ContentValues;
import android.database.Cursor;
import android.media.RingtoneManager;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;

/**
 * This is the data access object (DAO) for the application. It queries
 * ringtones, contacts, and groups and creates data structures representing
 * their relationships. This DAO provides the necessary methods for sorting and
 * filtering ringtones, contacts, and groups for display purposes. Mutator
 * methods are provided to set the default ringtone and for setting the ringtone
 * for a contact or group of contacts.
 * 
 * <pre>
 * {@code}
 * DAO d = DAO.getInstance(this);
 * Ringtone[] ringtones = d.getRingtones(Ringtone.Filter.ALL, Ringtone.Sort.NAME);
 * 
 * for (Ringtone r : ringtones) {
 *     System.out.println("Ringtone " + r.getName() + " has " + r.getNumContacts() + " contacts.");
 * }
 * 
 * // Set the first ringtone to default
 * d.setDefault(ringtones[0]);
 * 
 * 
 * Contact[] contacts = d.getContacts(Contact.Filter.CUSTOM, Contact.Sort.RINGTONE);
 * // Set all contacts with custom ringtones to use the default ringtone.
 * for (Contact contact : contacts) {
 *     d.setDefault(contact);
 * }
 * </pre>
 */
public class DAO {
    private static class DAOHolder {
        public static final DAO instance = new DAO();
    }

    private class SecretContact implements Contact {
        private String _id;
        private String _name;
        private SecretRingtone _ringtone;
        private String _ringtoneId;

        public SecretContact(String id, String name, String ringtoneId) {
            _id = id;
            _name = name;
            _ringtoneId = ringtoneId;
        }

        @Override
        public String getId() {
            return _id;
        }

        @Override
        public String getName() {
            return _name;
        }

        @Override
        public Ringtone getRingtone() {
            return _ringtone;
        }

        @Override
        public String getRingtoneId() {
            return _ringtoneId;
        }

        @Override
        public boolean isDirty() {
            return _ringtoneId.compareTo("") != 0 && _ringtoneId.compareTo(_ringtone.getId()) != 0;
        }

        @Override
        public boolean isSetToDefault() {
            return _ringtoneId.compareTo("") == 0;
        }

        public void setRingtone(SecretRingtone ringtone) {
            _ringtone = ringtone;
        }

        public void setRingtoneId(String id) {
            _ringtoneId = id;
        }

        @Override
        public String toString() {
            return _name;
        }
    }

    private class SecretGroup implements Group {
        private List<SecretContact> _contacts;
        private String _id;
        private String _name;
        private String mAccountType;
        private String mAccountName;
        private String mDataSet;
        private Set<SecretRingtone> mRingtones;

        public SecretGroup(String id, String name, String accountType, String accountName,
                String dataSet) {
            _id = id;
            _name = name;
            mAccountType = accountType;
            mAccountName = accountName;
            mDataSet = dataSet;
            _contacts = new ArrayList<SecretContact>();
            mRingtones = new java.util.HashSet<SecretRingtone>();
        }

        public boolean add(SecretContact contact) {
            // TODO keep track of all the different ringtones used by the
            // contacts?
            mRingtones.add((SecretRingtone) contact.getRingtone());
            return _contacts.add(contact);
        }

        @Override
        public Contact[] getContacts() {
            Contact[] contacts = new Contact[_contacts.size()];
            return (Contact[]) _contacts.toArray(contacts);
        }

        @Override
        public String getId() {
            return _id;
        }

        @Override
        public String getName() {
            return _name;
        }

        @Override
        public Ringtone[] getRingtones() {
            return mRingtones.toArray(new Ringtone[mRingtones.size()]);
        }

        @Override
        public String getAccount() {
            // TODO build a string like Google - email@gmail.com or Google+ -
            // email@gmail.com
            return (this.mDataSet == null) ? "Gmail" : "Google+";
        }
    }

    private class SecretRingtone implements Ringtone {
        private List<SecretContact> _contacts;
        private String _id;
        private boolean _isDefault;
        private String _name;
        private int _position;

        public SecretRingtone(String id, int position, boolean isDefault, String name) {
            _id = id;
            _position = position;
            _isDefault = isDefault;
            _name = name;
            _contacts = new ArrayList<SecretContact>();
        }

        public boolean add(SecretContact contact) {
            contact.setRingtone(this);
            return _contacts.add(contact);
        }

        public boolean addAll(SecretContact[] contacts) {
            for (SecretContact contact : contacts) {
                contact.setRingtone(this);
            }

            // TODO Do I need to worry about order?
            return _contacts.addAll(Arrays.asList(contacts));
        }

        @Override
        public Contact[] getContacts() {
            Contact[] contacts = new Contact[_contacts.size()];
            return (Contact[]) _contacts.toArray(contacts);
        }

        @Override
        public String getId() {
            return _id;
        }

        @Override
        public String getName() {
            return _name;
        }

        @Override
        public int getNumContacts() {
            return _contacts.size();
        }

        @Override
        public int getPosition() {
            return _position;
        }

        @Override
        public boolean isDefault() {
            return _isDefault;
        }

        public boolean removeContact(SecretContact contact) {
            return _contacts.remove(contact);
        }

        public SecretContact[] removeDefaults() {
            List<SecretContact> retVal = new ArrayList<SecretContact>();

            for (Iterator<SecretContact> iter = _contacts.iterator(); iter.hasNext();) {
                SecretContact contact = iter.next();

                if (contact.isSetToDefault() || contact.isDirty()) {
                    contact.setRingtone(null);
                    retVal.add(contact);
                    iter.remove();
                }
            }

            return retVal.toArray(new SecretContact[retVal.size()]);
        }

        public void setDefault(boolean isDefault) {
            _isDefault = isDefault;
        }

        @Override
        public String toString() {
            return _name;
        }
    }

    static private Activity sActivity;

    /**
     * @param activity
     *            The context in which querying will be done.
     * @return The singleton instance of the DAO.
     */
    public static DAO getInstance(Activity activity) {
        sActivity = activity;

        return DAOHolder.instance;
    }

    private Contact.Filter mContactFilter;
    private Map<String, SecretContact> mContacts;
    private Contact.Sort mContactSort;
    private SecretRingtone mDefaultRingtone;
    private Group.Filter mGroupFilter;
    private Map<String, SecretGroup> mGroups;
    private Group.Sort mGroupSort;
    private Ringtone.Filter mRingtoneFilter;
    private RingtoneManager mRingtoneManager;
    private Map<String, SecretRingtone> mRingtones;
    private Ringtone.Sort mRingtoneSort;

    private DAO() {
        mRingtoneManager = new RingtoneManager(sActivity);
        mRingtoneManager.setIncludeDrm(true);
        mRingtoneManager.setStopPreviousRingtone(true);

        mContacts = new LinkedHashMap<String, SecretContact>();
        mRingtones = new LinkedHashMap<String, SecretRingtone>();
        mGroups = new LinkedHashMap<String, SecretGroup>();

        // TODO use SharedPreferences to get/set the last filters/sorts
        // http://developer.android.com/guide/topics/data/data-storage.html#pref
        mRingtoneFilter = Ringtone.Filter.ALL;
        mRingtoneSort = Ringtone.Sort.CONTACT_COUNT;

        loadRingtones();
        loadContacts();
        loadGroups();
    }

    /**
     * @return An array of {@link Contact} objects that have been sorted and
     *         filtered per the last used sort and filter.
     */
    public Contact[] getContacts() {
        return getContacts(mContactFilter, mContactSort);
    }

    /**
     * @param filter
     *            Used to determine which {@link Contact}s will be returned.
     * @return An array of {@link Contact} objects that have been sorted by the
     *         last used sort and filtered per the supplied parameter.
     */
    public Contact[] getContacts(Contact.Filter filter) {
        mContactFilter = filter;
        return getContacts(mContactFilter, mContactSort);
    }

    /**
     * @param filter
     *            Used to determine which {@link Contact}s will be returned.
     * @param sort
     *            Used to determine the sort order of the {@link Contact}s that
     *            are returned.
     * @return An array of {@link Contact} objects that have been sorted and
     *         filtered per the supplied parameters.
     * @see Contact.Filter
     * @see Contact.Sort
     * @throws IllegalArgumentException
     *             If {@code filter} or {@code sort} are not acceptable values.
     */
    public Contact[] getContacts(Contact.Filter filter, Contact.Sort sort) {
        List<Contact> tempList = new ArrayList<Contact>(mContacts.size());
        Comparator<Contact> comparator;

        mContactFilter = filter;
        mContactSort = sort;

        switch (filter) {
        case ALL:
            tempList.addAll(mContacts.values());
            break;
        case CUSTOM:
            for (SecretContact contact : mContacts.values()) {
                if (!contact.isSetToDefault() && !contact.isDirty()) {
                    tempList.add(contact);
                }
            }
            break;
        case DEFAULT:
            for (SecretContact contact : mContacts.values()) {
                if (contact.isSetToDefault()) {
                    tempList.add(contact);
                }
            }
            break;
        case DIRTY:
            for (SecretContact contact : mContacts.values()) {
                if (contact.isDirty()) {
                    tempList.add(contact);
                }
            }
            break;
        default:
            throw new IllegalArgumentException("Invalid parameter 'filter'.");
        }

        switch (sort) {
        case NAME:
            // TODO i should be able to assume these are already sorted by name,
            // tighten up.
            comparator = new Comparator<Contact>() {
                @Override
                public int compare(Contact a, Contact b) {
                    return a.getName().toUpperCase().compareTo(b.getName().toUpperCase());
                }
            };
            break;
        case RINGTONE:
            comparator = new Comparator<Contact>() {
                @Override
                public int compare(Contact a, Contact b) {
                    if (a.getRingtone().equals(b.getRingtone())) {
                        return a.getName().toUpperCase().compareTo(b.getName().toUpperCase());
                    } else {
                        return a.getRingtone().getName().toUpperCase()
                                .compareTo(b.getRingtone().getName().toUpperCase());
                    }
                }
            };
            break;
        default:
            throw new IllegalArgumentException("Invalid parameter 'sort'.");
        }

        Collections.sort(tempList, comparator);

        return tempList.toArray(new Contact[tempList.size()]);
    }

    /**
     * @param sort
     *            Used to determine the sort order of the {@link Contact}s that
     *            are returned.
     * @return An array of {@link Contact} objects that have been sorted per the
     *         supplied parameter and filtered by the last used filter.
     */
    public Contact[] getContacts(Contact.Sort sort) {
        mContactSort = sort;
        return getContacts(mContactFilter, mContactSort);
    }

    /**
     * Get all contacts. Sort the contacts such that contacts that are assigned
     * to the given ringtone will appear before those that are not. Contacts are
     * then sorted by name.
     * 
     * @param ringtone
     *            The ringtone used to determine which contacts are first in the
     *            returned list. The contacts that are assigned this ringtone
     *            will be listed first, sorted by name.
     * @return An array of all contacts that are sorted with contacts assigned
     *         the given ringtone first.
     */
    public Contact[] getContacts(final Ringtone ringtone) {
        Contact[] retVal = mContacts.values().toArray(new Contact[mContacts.size()]);
        Comparator<Contact> comparator = new Comparator<Contact>() {
            @Override
            public int compare(Contact lhs, Contact rhs) {
                if (lhs.getRingtoneId().compareTo(ringtone.getId()) == 0
                        && rhs.getRingtoneId().compareTo(ringtone.getId()) != 0) {
                    return -1;
                } else if (lhs.getRingtoneId().compareTo(ringtone.getId()) != 0
                        && rhs.getRingtoneId().compareTo(ringtone.getId()) == 0) {
                    return 1;
                } else {
                    return lhs.getName().toUpperCase().compareTo(rhs.getName().toUpperCase());
                }
            }
        };

        Arrays.sort(retVal, comparator);

        return retVal;
    }

    /**
     * @return An array of {@link Group} objects that have been sorted and
     *         filtered per the last used sort and filter.
     */
    public Group[] getGroups() {
        return getGroups(mGroupFilter, mGroupSort);
    }

    /**
     * @param filter
     *            Used to determine which {@link Group}s will be returned.
     * @return An array of {@link Group} objects that have been sorted by the
     *         last used sort and filtered per the supplied parameter.
     */
    public Group[] getGroups(Group.Filter filter) {
        mGroupFilter = filter;
        return getGroups(mGroupFilter, mGroupSort);
    }

    /**
     * @param filter
     *            Used to determine which {@link Group}s will be returned.
     * @param sort
     *            Used to determine the sort order of the {@link Group}s that
     *            are returned.
     * @return An array of {@link Group} objects that have been sorted and
     *         filtered per the supplied parameters.
     * @see Group.Filter
     * @see Group.Sort
     * @throws IllegalArgumentException
     *             If {@code filter} or {@code sort} are not acceptable values.
     */
    public Group[] getGroups(Group.Filter filter, Group.Sort sort) {
        List<Group> tempList = new ArrayList<Group>(mGroups.size());
        Comparator<Group> comparator;

        // TODO Add filtered groups to tempList
        switch (filter) {
        case ALL:
            tempList.addAll(mGroups.values());
        case CUSTOM:
            break;
        default:
            throw new IllegalArgumentException("Invalid parameter 'filter'.");
        }

        switch (sort) {
        case NAME:
            // TODO verify that the groups will be sorted by name (check for
            // upper vs. lower case)
            comparator = new Comparator<Group>() {
                @Override
                public int compare(Group a, Group b) {
                    return a.getName().toUpperCase().compareTo(b.getName().toUpperCase());
                }
            };
            break;
        case TODO_UNKNOWN_OTHER:
            // TODO How should groups be sorted?
        default:
            throw new IllegalArgumentException("Invalid parameter 'sort'.");
        }

        Collections.sort(tempList, comparator);

        return tempList.toArray(new Group[tempList.size()]);
    }

    /**
     * @param sort
     *            Used to determine the sort order of the {@link Group}s that
     *            are returned.
     * @return An array of {@link Group} objects that have been sorted per the
     *         supplied parameter and filtered by the last used filter.
     */
    public Group[] getGroups(Group.Sort sort) {
        mGroupSort = sort;
        return getGroups(mGroupFilter, mGroupSort);
    }

    /**
     * Get all groups. Sort the groups such that groups that are assigned to the
     * given ringtone will appear before those that are not. Groups are then
     * sorted by name.
     * 
     * @param ringtone
     *            The ringtone used to determine which groups are first in the
     *            returned list. The groups that are assigned this ringtone will
     *            be listed first, sorted by name.
     * @return An array of all groups that are sorted with groups assigned the
     *         given ringtone first.
     */
    public String[] getGroups(Ringtone ringtone) {
        String[] groupList = new String[mGroups.size()];
        int index = -1;

        for (Iterator<SecretGroup> iter = mGroups.values().iterator(); iter.hasNext();) {
            SecretGroup group = iter.next();
            groupList[++index] = group.getName();
        }

        // TODO since i plan on using LinkedHashMap, i can assume predictable
        // iteration order. Therefore if I were to sort the content providers by
        // Name I could then make an assumption throughout that the collection
        // is already sorted by name. This should also be extended to Contacts
        // and Ringtones.
        Arrays.sort(groupList);

        return groupList;
    }

    /**
     * @return An array of {@link Ringtone} objects that have been filtered and
     *         sorted the same as the last call to any getRingtones method, or
     *         by the default filter and sort.
     */
    public Ringtone[] getRingtones() {
        return getRingtones(mRingtoneFilter, mRingtoneSort);
    }

    /**
     * @param filter
     *            Used to determine which {@link Ringtone}s will be returned.
     * @return An array of {@link Ringtone} objects that have been filtered per
     *         the supplied parameter. The list will be sorted by the last sort
     *         used or the default if no sort has been set.
     */
    public Ringtone[] getRingtones(Ringtone.Filter filter) {
        mRingtoneFilter = filter;
        return getRingtones(mRingtoneFilter, mRingtoneSort);
    }

    /**
     * @param filter
     *            Used to determine which {@link Ringtone}s will be returned.
     * @param sort
     *            Used to determine the sort order of the {@link Ringtone}s that
     *            are returned.
     * @return An array of {@link Ringtone} objects that have been sorted and
     *         filtered per the supplied parameters.
     * @see Ringtone.Filter
     * @see Ringtone.Sort
     * @throws IllegalArgumentException
     *             If {@code filter} or {@code sort} are not acceptable values.
     */
    public Ringtone[] getRingtones(Ringtone.Filter filter, Ringtone.Sort sort) {
        List<Ringtone> tempList = new ArrayList<Ringtone>(mRingtones.size());
        Comparator<Ringtone> comparator;

        mRingtoneFilter = filter;
        mRingtoneSort = sort;

        switch (mRingtoneFilter) {
        case ALL:
            tempList.addAll(mRingtones.values());
            break;
        case WITH_CONTACT:
            for (SecretRingtone ringtone : mRingtones.values()) {
                if (ringtone.getNumContacts() > 0) {
                    tempList.add(ringtone);
                }
            }
            break;
        case WITHOUT_CONTACT:
            for (SecretRingtone ringtone : mRingtones.values()) {
                if (ringtone.getNumContacts() == 0) {
                    tempList.add(ringtone);
                }
            }
            break;
        default:
            throw new IllegalArgumentException("Invalid parameter 'filter'.");
        }

        switch (mRingtoneSort) {
        case CONTACT_COUNT:
            comparator = new Comparator<Ringtone>() {
                @Override
                public int compare(Ringtone a, Ringtone b) {
                    if (b.getNumContacts() == a.getNumContacts()) {
                        return a.getName().toUpperCase().compareTo(b.getName().toUpperCase());
                    } else {
                        return b.getNumContacts() - a.getNumContacts();
                    }
                }
            };
            break;
        case NAME:
            // TODO make changes so that it can be assumed that mRingtones is
            // sorted by name already, then don't do this sort.
            comparator = new Comparator<Ringtone>() {
                @Override
                public int compare(Ringtone a, Ringtone b) {
                    return a.getName().toUpperCase().compareTo(b.getName().toUpperCase());
                }
            };
            break;
        default:
            throw new IllegalArgumentException("Invalid parameter 'sort'.");
        }

        Collections.sort(tempList, comparator);

        return tempList.toArray(new Ringtone[tempList.size()]);
    }

    /**
     * @param sort
     *            Used to determine the sort order of the {@link Ringtone}s that
     *            are returned.
     * @return An array of {@link Ringtone} objects that have been sorted per
     *         the supplied parameter. The list will be filtered by the last
     *         filter used or the default if no filter has been used.
     */
    public Ringtone[] getRingtones(Ringtone.Sort sort) {
        mRingtoneSort = sort;
        return getRingtones(mRingtoneFilter, mRingtoneSort);
    }

    /**
     * This method loads {@link DAO#mContacts} as well as inserts those contacts
     * into the corresponding {@link Ringtone} in {@link DAO#mRingtones}. You
     * can assume throughout this class that {@link DAO#mContacts} will be
     * sorted by the name of the contact.
     * 
     * <p>
     * See the description of the following methods for how this method should
     * behave.
     * </p>
     * 
     * @see Contact#getRingtoneId()
     * @see Contact#getRingtone()
     * @see Contact#isSetToDefault()
     * @see Contact#isDirty()
     */
    private void loadContacts() {
        SecretContact newContact;

        Cursor contactCursor = sActivity.getContentResolver().query(Contacts.CONTENT_URI,
                new String[] { Contacts._ID, Contacts.DISPLAY_NAME, Contacts.CUSTOM_RINGTONE },
                Contacts.HAS_PHONE_NUMBER + "='1'", null, Contacts.DISPLAY_NAME);

        SecretRingtone currentRingtone;

        while (contactCursor.moveToNext()) {
            newContact = new SecretContact(contactCursor.getString(0), contactCursor.getString(1),
                    (contactCursor.isNull(2)) ? "" : contactCursor.getString(2));

            mContacts.put(newContact.getId(), newContact);
            currentRingtone = mRingtones.get(newContact.getRingtoneId());

            if (currentRingtone == null) {
                currentRingtone = mDefaultRingtone;
            } else {
                currentRingtone.add(newContact);
            }

            newContact.setRingtone(currentRingtone);
        }

        contactCursor.close();

        // TODO State that contacts are sorted by name. do i have to worry about
        // case?
    }

    private void loadGroups() {
        SecretGroup newGroup, tempGroup;
        SecretContact tempContact;
        Cursor groupCursor;

        groupCursor = sActivity.getContentResolver().query(
                Groups.CONTENT_SUMMARY_URI,
                new String[] { Groups._ID, Groups.TITLE, Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME,
                        Groups.DATA_SET, Groups.SUMMARY_WITH_PHONES },
                Groups.SUMMARY_WITH_PHONES + ">0", null, Groups.TITLE);

        // ACCOUNT_TYPE -> com.google
        // ACCOUNT_NAME -> email.address@gmail.com
        // DATA_SET -> null (for Gmail Group) or "plus" (for Google+ group)

        while (groupCursor.moveToNext()) {
            newGroup = new SecretGroup(groupCursor.getString(0), groupCursor.getString(1),
                    groupCursor.getString(2), groupCursor.getString(3), groupCursor.getString(4));
            mGroups.put(newGroup.getId(), newGroup);
        }

        groupCursor.close();

        groupCursor = sActivity.getContentResolver().query(
                Data.CONTENT_URI,
                new String[] { GroupMembership.GROUP_ROW_ID, GroupMembership.CONTACT_ID },
                GroupMembership.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND "
                        + GroupMembership.HAS_PHONE_NUMBER + " = '1'", null,
                GroupMembership.DISPLAY_NAME);

        while (groupCursor.moveToNext()) {
            // TODO the indices will be determined by how the query is setup
            tempGroup = mGroups.get(groupCursor.getString(0));
            tempContact = mContacts.get(groupCursor.getString(1));

            tempGroup.add(tempContact);
        }

        groupCursor.close();

        // TODO should i guarantee the groups are sorted by name?
    }

    private void loadRingtones() {
        SecretRingtone newRingtone;
        RingtoneManager bob = new RingtoneManager(sActivity);
        bob.setIncludeDrm(true);
        Cursor c = bob.getCursor();
        String defaultRingtoneId = RingtoneManager.getActualDefaultRingtoneUri(sActivity,
                RingtoneManager.TYPE_RINGTONE).toString();

        while (c.moveToNext()) {
            String id = c.getString(RingtoneManager.URI_COLUMN_INDEX).concat("/")
                    .concat(c.getString(RingtoneManager.ID_COLUMN_INDEX));

            newRingtone = new SecretRingtone(id, c.getPosition(),
                    id.compareTo(defaultRingtoneId) == 0,
                    c.getString(RingtoneManager.TITLE_COLUMN_INDEX));

            mRingtones.put(id, newRingtone);

            if (id.compareTo(defaultRingtoneId) == 0) {
                mDefaultRingtone = newRingtone;
            }
        }

        // TODO should i guarantee the ringtones are sorted by name?
    }

    /**
     * @param contact
     *            The contact to play the ringtone of. If
     *            {@link Contact#isDirty()} is true, then the default ringtone
     *            is played.
     */
    public void play(Contact contact) {
        play(contact.getRingtone());
    }

    /**
     * @param ringtone
     *            The ringtone to play.
     */
    public void play(Ringtone ringtone) {
        mRingtoneManager.getRingtone(ringtone.getPosition()).play();
    }

    /**
     * Set the custom ringtone of the given contact as the given ringtone.
     * 
     * @param ringtone
     *            The {@link Ringtone} to be used by the given {@link Contact}.
     * @param contact
     *            The {@link Contact} to have its custom ringtone set.
     */
    public void set(Ringtone ringtone, Contact contact) {
        SecretRingtone secretRingtone = mRingtones.get(ringtone.getId());
        SecretContact secretContact = mContacts.get(contact.getId());

        int rowsUpdated = setRingtone(secretRingtone.getId(), secretContact.getId());

        if (rowsUpdated == 1) {
            mRingtones.get(secretContact.getRingtone().getId()).removeContact(secretContact);
            secretContact.setRingtoneId(secretRingtone.getId());
            secretContact.setRingtone(secretRingtone);
            secretRingtone.add(secretContact);
        } else {
            // TODO this shouldn't happen, throw exception i guess?
        }
    }

    /**
     * @param ringtone
     *            The {@link Ringtone} to be used by all the {@link Contact}s in
     *            the given {@link Group}.
     * @param group
     *            The {link Group} of which all its {@link Contact}s will have
     *            their custom ringtones set.
     */
    public void set(Ringtone ringtone, Group group) {
        for (Contact contact : group.getContacts()) {
            set(ringtone, contact);
        }
    }

    public void set(Uri ringtoneUri, Contact contact) {
        SecretRingtone ringtone = mRingtones.get(ringtoneUri.toString());
        set(ringtone, contact);
    }

    /**
     * @param contact
     *            The {@link Contact} to set it to use the default ringtone.
     */
    public void setDefault(Contact contact) {
        SecretContact secretContact = mContacts.get(contact.getId());

        int rowsUpdated = setRingtone("", secretContact.getId());

        if (rowsUpdated == 1) {
            mRingtones.get(secretContact.getRingtone().getId()).removeContact(secretContact);
            secretContact.setRingtoneId("");
            secretContact.setRingtone(mDefaultRingtone);
        } else {
            // TODO this shouldn't happen, throw exception i guess?
        }
    }

    /**
     * @param group
     *            The {@link Group} to set all contacts in that group to use the
     *            default ringtone.
     */
    public void setDefault(Group group) {
        for (Contact contact : group.getContacts()) {
            setDefault(contact);
        }
    }

    /**
     * @param ringtone
     *            The {@link Ringtone} to be set as the default ringtone for the
     *            phone.
     */
    public void setDefault(Ringtone ringtone) {
        RingtoneManager.setActualDefaultRingtoneUri(sActivity, RingtoneManager.TYPE_RINGTONE,
                Uri.parse(ringtone.getId()));

        SecretRingtone oldDefault = mDefaultRingtone;
        SecretRingtone newDefault = mRingtones.get(ringtone.getId());

        newDefault.addAll(oldDefault.removeDefaults());

        oldDefault.setDefault(false);
        newDefault.setDefault(true);

        mDefaultRingtone = newDefault;
    }

    private int setRingtone(String ringtoneId, String contactId) {
        ContentValues mUpdateValues = new ContentValues();

        String mSelectionClause = Contacts._ID + "= ?";
        String[] mSelectionArgs = { contactId };

        mUpdateValues.put(Contacts.CUSTOM_RINGTONE, ringtoneId);

        return sActivity.getContentResolver().update(Contacts.CONTENT_URI, mUpdateValues,
                mSelectionClause, mSelectionArgs);
    }

    /**
     * Stops the currently playing ringtone.
     */
    public void stop() {
        mRingtoneManager.stopPreviousRingtone();
    }
}
